Skip to content

chore(lint): enforce tailwind design tokens#241

Open
wheeljackz wants to merge 1 commit intomainfrom
feat/card-265-tailwind-token-enforcement
Open

chore(lint): enforce tailwind design tokens#241
wheeljackz wants to merge 1 commit intomainfrom
feat/card-265-tailwind-token-enforcement

Conversation

@wheeljackz
Copy link
Copy Markdown
Collaborator

@wheeljackz wheeljackz commented Mar 30, 2026

Summary

  • add eslint-plugin-tailwindcss pinned to a version compatible with the repo's current ESLint and Tailwind setup
  • enable the Tailwind arbitrary value lint rule and point it at the local Tailwind config
  • verify pre-commit enforcement through lint-staged so arbitrary classes like bg-[#fff] are rejected

Verification

  • npm run lint
  • npm test
  • printf 'export function Demo(){return <div className="bg-[#fff] p-4" />}\n' | npx eslint --stdin --stdin-filename app/__tailwind-smoke.tsx
  • npx lint-staged with a temporary staged TSX file containing bg-[#fff], confirmed failure from tailwindcss/no-arbitrary-value

Copy link
Copy Markdown
Owner

@zainfathoni zainfathoni left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: chore(lint): enforce tailwind design tokens

✅ ESLint Plugin Configuration — Looks Good

  • tailwindcss correctly added to plugins array
  • tailwindcss/no-arbitrary-value: "error" properly enforces design token usage
  • settings.tailwindcss.config correctly points to tailwind.config.js
  • Choosing a single rule (no-arbitrary-value) instead of extends: ["plugin:tailwindcss/recommended"] is a reasonable scoped approach

✅ Pre-commit Enforcement — Already Covered

No changes needed here — the existing lint-staged config already runs eslint --cache --fix on *.{js,jsx,ts,tsx,yml,yaml}, so the new rule is automatically enforced on pre-commit via Husky. Good that the PR didnt add unnecessary config.

⚠️ Minor Observations

  1. Version range vs pinning: The PR description says the plugin is "pinned to a version compatible," but package.json uses ^3.5.2 (caret range), which allows 3.x.x upgrades. The lockfile pins it to 3.5.2 today, but a future npm install could pull a newer minor. If strict pinning is intended, use "3.5.2" (no caret). Low risk since lockfile is committed — just a consistency note.

  2. Existing code compliance: The no-arbitrary-value rule is strict — it rejects all arbitrary Tailwind classes like bg-[#fff], w-[200px], text-[14px], etc. The diff shows no source file changes, which means either:

    • The codebase already uses no arbitrary values (great!) ✅
    • Or existing violations will cause npm run lint to fail immediately ❌

    The PR description mentions verification via npm run lint passing, so this seems fine — but worth double-checking CI passes before merge.

  3. Lock file churn: The diff includes some postcss-js and yaml package hoisting changes (moved from tailwindcss/node_modules/ to top-level). This is normal npm deduplication behavior — not a concern, just noting it.

Verdict

Clean, minimal, well-scoped change. The configuration is correct and pre-commit enforcement works through existing infrastructure. No breaking changes if the codebase already passes the new rule (which the PR verification suggests it does). 👍

Copy link
Copy Markdown
Collaborator Author

@wheeljackz wheeljackz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I validated the Tailwind rule against the current repo patterns and isolated ESLint repros. I did not find any existing arbitrary-value classes in app/, but I did find two issues with the proposed enforcement: it misses our local classNames() helper, and it flags valid Tailwind arbitrary variants like data-[state=open]:pb-8. Posting those inline below.

Comment thread .eslintrc.json
"import/order": "error",
"react/react-in-jsx-scope": ["off"]
"react/react-in-jsx-scope": ["off"],
"tailwindcss/no-arbitrary-value": "error"
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tailwindcss/no-arbitrary-value is broader than "design token enforcement" here: it also rejects valid Tailwind arbitrary variants/modifiers, not just raw values. I reproduced this locally with <div className="data-[state=open]:pb-8" />, which fails with Arbitrary value detected in data-[state=open]:pb-8. Because this repo already depends on Headless UI, that is a realistic false-positive path for standard Tailwind state selectors, and it will force inline disables as soon as we adopt them.

Comment thread .eslintrc.json
"react": {
"version": "detect"
},
"tailwindcss": {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn’t fully enforce the rule across the current codebase because the plugin’s default callees list does not include our local classNames() helper. I counted 32 classNames( call sites in app/, and I reproduced that className={classNames(bg-[#fff], p-4)} passes lint while the same sample with clsx(...) fails. Please add settings.tailwindcss.callees with classNames (or migrate those call sites), otherwise arbitrary values still slip through many existing composition paths.

Copy link
Copy Markdown
Collaborator Author

@wheeljackz wheeljackz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found 1 blocking gap in the new Tailwind lint setup. correctly catches direct JSX class strings and calls, but the current config does not cover this repo's helper, so a large share of composed classes can still bypass the rule. I verified that passes lint while the same class in JSX or fails. I did not find existing bracketed Tailwind classes in the repo, so I don't see an immediate false-positive issue on current code, but enforcement is incomplete until the helper is included.

Comment thread .eslintrc.json
"react": {
"version": "detect"
},
"tailwindcss": {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plugin only inspects call expressions whose callee is listed in settings.tailwindcss.callees, and its default list is just classnames, clsx, and ctl. This repo builds many class strings with classNames(...) (for example app/components/button-link.tsx and app/components/page-link.tsx), so arbitrary values inside those calls currently bypass the new rule. I verified that className={classNames("bg-[#fff]", "p-4")} passes lint while the same class in JSX or clsx(...) fails. Please add classNames here or the enforcement will miss a substantial part of the codebase.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants